package org.zoomdev.zoom.common.res.impl;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zoomdev.zoom.common.exceptions.ZoomException;
import org.zoomdev.zoom.common.filter.Filter;
import org.zoomdev.zoom.common.filter.impl.AnnotationFilter;
import org.zoomdev.zoom.common.filter.impl.OrFilter;
import org.zoomdev.zoom.common.filter.pattern.PatternFilterFactory;
import org.zoomdev.zoom.common.io.Io;
import org.zoomdev.zoom.common.res.ResScanner;
import org.zoomdev.zoom.common.utils.CachedClasses;
import org.zoomdev.zoom.common.utils.PathUtils;
import org.zoomdev.zoom.common.utils.Visitor;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public final class ResScannerImpl implements ResScanner {


    private static final Logger log = LoggerFactory.getLogger(ResScannerImpl.class);
    /**
     * 默认不扫描jar文件,除非是zoom*.jar
     */
    public static Filter<File> fastFilter = value -> {
        String name = value.getName();
        return !name.endsWith("jar") || (name.startsWith("zoom") && name.endsWith("jar"));
    };
    private final List<Res> files = new ArrayList<>();
    Set<ClassResImpl> classes = new HashSet<>();

    private Filter<File> jarFilter;
    private Filter<String> scanFilter;

    public ResScannerImpl() {
    }

    public void appendRes(ClassResImpl res) {
        classes.add(res);
    }

    public void clear() {
        classes.clear();
    }

    @Override
    public void destroy() {
        clear();

    }

    public List<Class> findClassByClass(Class<? extends Annotation> annotationClass) {
        return findClassByClass(new AnnotationFilter(annotationClass));
    }

    public List<Class> findClassByClass(Filter<Class> filter) {
        List<Class> result = new ArrayList<>();
        for (ClassResImpl res : classes) {
            if (filter.accept(res.getType())) {
                result.add(res.getType());
            }
        }
        return result;
    }

    /**
     * 扫描目录
     *
     * @param root
     * @param folder
     * @throws IOException
     */
    public void scanFolder(String root, ClassLoader classLoader, File folder) throws IOException {
        File[] files = folder.listFiles();
        if (files == null) {
            return;
        }

        for (File file : files) {
            if (file.isDirectory()) {
                scanFolder(root, classLoader, file);
            } else {
                parseFile(root, classLoader, file);
            }
        }
    }

    /**
     * 解析jar文件
     *
     * @param file
     * @throws IOException
     */
    public void parseJar(File file, ClassLoader classLoader) throws IOException {
        if (log.isDebugEnabled()) {
            log.debug("Reading jar file:" + file.getAbsolutePath());
        }
        if (!file.exists()) {
            log.error("Jar file not exists:" + file.getAbsolutePath());
            return;
        }
        ZipInputStream inputStream = null;
        try {
            InputStream is = new FileInputStream(file);
            inputStream = new ZipInputStream(is);
            ZipEntry entry;
            while ((entry = inputStream.getNextEntry()) != null) {
                String name = entry.getName();
                if (name.endsWith(".class")) {
                    addJarClass(getClassNameInJarStream(name), classLoader, file);
                } else {
                    if (entry.isDirectory()) {
                        continue;
                    }
                    addJarFile(name, classLoader, file);
                }
            }

        } finally {
            Io.close(inputStream);
        }
    }

    public Filter<File> getJarFilter() {
        return jarFilter;
    }

    public void setFilter(String scanFilter) {
        assert (!StringUtils.isBlank(scanFilter));
        this.scanFilter = PatternFilterFactory.createFilter(scanFilter);
    }

    public void scan() throws IOException {
        scan(getClass().getClassLoader(), fastFilter);
    }

    /**
     * @param classLoader
     * @throws IOException
     */
    public void scan(ClassLoader classLoader) throws IOException {
        scan(classLoader, fastFilter);
    }

    /**
     * 扫描一个jar文件
     *
     * @param is
     * @param classLoader
     * @throws IOException
     */
    public void scan(InputStream is, ClassLoader classLoader) throws IOException {
        ZipInputStream inputStream = null;
        try {
            inputStream = new ZipInputStream(is);
            ZipEntry entry;
            while ((entry = inputStream.getNextEntry()) != null) {
                String name = entry.getName();
                if (name.endsWith("class")) {
                    addStreamClass(inputStream, getClassNameInJarStream(name), classLoader);
                }
            }

        } finally {
            Io.close(inputStream);
        }

    }

    /**
     * 扫描项目所有的资源文件
     *
     * @param classLoader
     * @param jarFilter   如果需要限制文件，自定义filter
     * @throws IOException
     */
    public void scan(ClassLoader classLoader, Filter<File> jarFilter) throws IOException {
        assert (jarFilter != null);

        this.jarFilter = jarFilter;
        /// 如果有缓存，那么清理一下
        clear();

        Collection<String> sources = PathUtils.getSources();
        if (PathUtils.isInJar) {
            String firstJar = null;
            for (String str : sources) {
                if (str.endsWith(".jar")) {
                    firstJar = str;
                }
            }
            this.jarFilter = new OrFilter<>(this.jarFilter, new EqFilter(firstJar));
        }

        for (String path : sources) {
            File file = new File(path);
            if (file.isDirectory()) {
                scanFolder(path, classLoader, file);
            } else {
                parseFile(path, classLoader, file);
            }
        }

    }

    /**
     * 如果你能确定只有这么一个文件叫这个名字，那么精确匹配
     *
     * @param name
     * @return
     */
    public Res getFile(String name) {
        assert (!StringUtils.isEmpty(name));
        // 资源文件应该没有几个
        for (Res res : files) {
            if (name.equals(res.getName())) {
                return res;
            }
        }

        return null;
    }

    /**
     * 如果你能确定只有一个，那么用className精确匹配 搜索顺序为 class 文件 -> jar文件
     *
     * @param className
     * @return
     */
    public ClassRes getClass(String className) {
        assert (!StringUtils.isEmpty(className));


        // 资源文件应该没有几个
        for (ClassResImpl res : classes) {
            if (className.equals(res.getName())) {
                return res;
            }
        }

        return null;
    }

    /**
     * 如果上述的搜索条件满足不了，那么就可以自定义搜索
     *
     * @param filter
     * @return
     */
    public List<Res> findFile(Filter<Res> filter) {
        List<Res> res = new ArrayList<>();
        for (Res file : files) {
            if (filter.accept(file)) {
                res.add(file);
            }
        }
        return res;
    }

    /**
     * 根据模式查找文件 *keyword* 模糊匹配含有keyword的资源 keyword* 模糊匹配以keyword开头的资源 *keyword
     * 模糊匹配以keyword结尾的资源
     *
     * @param pattern
     * @return
     */
    public List<Res> findFile(String pattern) {

        final Filter<String> patternFilter = PatternFilterFactory.createFilter(pattern);

        return findFile(value -> patternFilter.accept(value.getName()));
    }

    /**
     * 根据模式查找class,也会搜索到jar中的class ,注意不支持class中定义的内部class *com.a.b* 匹配含有com.a.b的类
     * com.a.b/* 匹配com.a.b这个包下的所有类,但是不含子包 com.a.b/** 匹配com.a.b这个包下的所有类,含有子包
     *
     * @param pattern
     * @return
     */
    public List<ClassRes> findClass(String pattern) {
        Filter<String> filter = PatternFilterFactory.createFilter(pattern);
        return findClass(filter);
    }

    public List<ClassRes> findClass(Filter<String> pattern) {
        List<ClassRes> result = new ArrayList<>();
        for (ClassResImpl res : classes) {
            if (pattern.accept(res.getName())) {
                result.add(res);
            }
        }
        return result;
    }

    public void visitClass(Visitor<ClassRes> visitor) {
        for (ClassResImpl res : classes) {
            visitor.visit(res);
        }
    }

    /**
     * 获取到jar文件里面的class文件的类名
     *
     * @param entryName
     * @return
     */
    private static String getClassNameInJarStream(String entryName) {
        return entryName.substring(0, entryName.length() - ".class".length()).replace("/", ".");
    }

    /**
     * 解析一个class文件对应的class名称
     *
     * @param root 扫描根目录
     * @param path class文件实际路径 如 root: /path/WEB-INF/classes path:
     *             /path/WEB-INF/classes/com/jzoom/Test.class 对应的结果为:com.jzoom.Test
     * @return
     */
    private static String getClassName(String root, String path) {
        String result = path.substring(root.length(), path.length() - 6).replace(File.separatorChar, '.');
        if (result.startsWith(".")) {
            return result.substring(1);
        }
        return result;
    }

    /**
     * 下个版本可能会进行解压缩
     *
     * @param inputStream
     * @param classNameInJarStream
     * @param classLoader
     */
    private void addStreamClass(
            ZipInputStream inputStream,
            String classNameInJarStream, ClassLoader classLoader) {

        classes.add(new ClassResImpl(classNameInJarStream, null, classLoader));
    }

    private void parseFile(String root, ClassLoader classLoader, File file) throws IOException {
        // 文件
        String name = file.getName();
        if (log.isTraceEnabled()) {
            log.trace("扫描到了文件" + file.getAbsolutePath());
        }
        // ALL FILE
        addFile(name, file);
        if (name.endsWith("class")) {
            parseClass(root, classLoader, file);
        } else if (name.endsWith("jar")) {
            if (jarFilter.accept(file)) {
                parseJar(file, classLoader);
            }
        } else {

        }

    }

    /**
     * 增加一个jar中的文件
     *
     * @param name
     * @param file
     */
    private void addJarFile(String name, ClassLoader classLoader, File file) {
        files.add(new JarFileRes(name, classLoader, file));
    }

    /**
     * 添加一个class文件
     *
     * @param className
     * @param file
     */
    private void addClass(String className, ClassLoader classLoader, File file) {

        classes.add(new ClassResImpl(className, file, classLoader));

    }

    /**
     * 添加一个jar中的class
     *
     * @param className
     * @param classLoader
     * @param jar
     */
    private void addJarClass(String className, ClassLoader classLoader, File jar) {
        if (className.contains("package-info")) {
            return;
        }
        if (log.isDebugEnabled()) {
            log.debug("Add jar class " + className + " in jar file:" + jar.getAbsolutePath());
        }
        classes.add(new JarClassResImpl(className, jar, classLoader));
    }

    /**
     * 解析其他的资源
     *
     * @param name
     * @param file
     */
    private void addFile(String name, File file) {
        files.add(new FileResImpl(file));
    }

    /**
     * @param root
     * @param file
     */
    private void parseClass(String root, ClassLoader classLoader, File file) {
        String className = getClassName(root, file.getAbsolutePath());
        if (className.contains("package-info")) {
            return;
        }

        if (scanFilter != null && !scanFilter.accept(className)) {
            return;
        }

        addClass(className, classLoader, file);
    }

    public interface JarResVisitor {
        void visit(ZipInputStream is, ZipEntry entry);
    }

    protected static class EqFilter implements Filter<File> {


        private final String path;

        public EqFilter(String path) {
            this.path = path;
        }

        @Override
        public boolean accept(File value) {
            return StringUtils.equals(value.getAbsolutePath(), this.path);
        }
    }

    static class FileResImpl implements FileRes {

        protected final File file;

        public FileResImpl(File file) {
            this.file = file;
        }

        @Override
        public File getFile() {
            return file;
        }

        @Override
        public String getName() {
            return file.getName();
        }

        @Override
        public InputStream getInputStream() throws IOException {
            return new FileInputStream(file);
        }

        @Override
        public byte[] getBytes() throws IOException {
            return Io.readBytes(file);
        }


        @Override
        public String toString() {
            return file.getName();
        }

    }

    public static class BytesClassResImpl extends ClassResImpl {

        public BytesClassResImpl(String className, byte[] file, ClassLoader classLoader) {
            super(className, null, classLoader);
        }


    }


    public static class ClassResImpl extends FileResImpl implements ClassRes {

        protected final String className;
        protected final ClassLoader classLoader;
        protected Class<?> type;
        protected Field[] fields;
        protected Method[] methods;
        int h;

        public ClassResImpl(String className, File file, ClassLoader classLoader) {
            super(file);
            assert (classLoader != null && className != null);
            this.classLoader = classLoader;
            this.className = className;
        }

        /**
         * 针对同一个class loader，想通的className认为是同一个类，否则认为不同类
         *
         * @return
         */
        @Override
        public int hashCode() {
            int h = this.h;
            if (h == 0) {
                h = 31 * h + classLoader.hashCode();
                h = 31 * h + className.hashCode();
                this.h = h;
            }
            return h;
        }


        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }

            if (obj instanceof ClassResImpl) {
                ClassResImpl other = (ClassResImpl) obj;
                return StringUtils.equals(className, other.className)
                        || this.classLoader == other.classLoader;

            }
            return false;
        }

        @Override
        public String getName() {
            return className;
        }

        @Override
        public String toString() {
            return className;
        }

        public ClassLoader getClassLoader() {
            return classLoader;
        }

        public Class<?> getType() {
            if (type == null) {
                type = loadClass();
            }
            return type;
        }

        public Field[] getFields() {
            if (fields == null) {
                fields = CachedClasses.getFields(type);
            }
            return fields;
        }

        public Method[] getPubMethods() {
            if (methods == null) {
                methods = CachedClasses.getPublicMethods(type);
            }
            return methods;
        }

        protected Class<?> loadClass() {
            try {
                return Class.forName(className, false, classLoader);
            } catch (ClassNotFoundException e) {
                throw new ZoomException("Cannot find class : " + className, e);
            }
        }
    }

    public class JarClassResImpl extends ClassResImpl {

        public JarClassResImpl(String className, File file, ClassLoader classLoader) {
            super(className, file, classLoader);
        }

        /// jar中的classinputstream,使用完毕必须要关闭
        @Override
        public InputStream getInputStream() throws IOException {
            ZipInputStream inputStream = new ZipInputStream(new FileInputStream(this.file));
            ZipEntry entry;
            while ((entry = inputStream.getNextEntry()) != null) {
                if (entry.isDirectory()) {
                    continue;
                }
                String name = entry.getName();
                if (StringUtils.endsWith(name, this.className)) {
                    return inputStream;
                }
            }
            return null;
        }

        public byte[] getBytes() throws IOException {
            String classPath = className.replace('.', '/') + ".class";
            try (JarFile jarFile = new JarFile(file)) {
                JarEntry entry = jarFile.getJarEntry(classPath);
                if (entry == null) {
                    throw new IOException("Class not found in JAR: " + className);
                }

                try (InputStream is = jarFile.getInputStream(entry)) {
                    return Io.readBytes(is);
                }
            }
        }

    }

    private class JarFileRes implements Res {

        private final File file;
        private final ClassLoader classLoader;
        private final String name;

        public JarFileRes(String name, ClassLoader classLoader, File file) {
            this.name = name;
            this.classLoader = classLoader;
            this.file = file;
        }

        @Override
        public File getFile() {
            return file;
        }

        @Override
        public String getName() {
            return name;
        }

        /// jar中的classinputstream,使用完毕必须要关闭
        @Override
        public InputStream getInputStream() throws IOException {
            ZipInputStream inputStream = new ZipInputStream(new FileInputStream(this.file));
            ZipEntry entry;
            while ((entry = inputStream.getNextEntry()) != null) {
                if (entry.isDirectory()) {
                    continue;
                }
                String name = entry.getName();
                if (StringUtils.endsWith(name, this.name)) {
                    return inputStream;
                }
            }
            return null;
        }

        public byte[] getBytes() throws IOException {
            String classPath = name.replace('.', '/') + ".class";
            try (JarFile jarFile = new JarFile(file)) {
                JarEntry entry = jarFile.getJarEntry(classPath);
                if (entry == null) {
                    throw new IOException("Class not found in JAR: " + name);
                }

                try (InputStream is = jarFile.getInputStream(entry)) {
                    return Io.readBytes(is);
                }
            }
        }
    }
}
